﻿using System;
using System.Reflection;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Interactivity;
using System.Windows.Media;
using WpfUIUtilities.Extensions;

namespace WpfUIUtilities.Behaviors
{
  /// <summary>
  /// Behavior used to bind the selected item of a tree view,
  /// which is not possible to bind as usual.
  /// </summary>
  public class BindableSelectedTreeViewItemBehavior : Behavior<TreeView>
  {
    /* Usage:
     <TreeView>
        <e:Interaction.Behaviors>
          <behaviours:BindableSelectedItemBehavior SelectedItem="{Binding SelectedItem, Mode=TwoWay}" />
        </e:Interaction.Behaviors>
      </TreeView>
     */

    #region dependency property SelectedItem

    /// <summary>
    /// Defines the selected item as a dependency property.
    /// </summary>
    public static readonly DependencyProperty SelectedItemProperty = 
      DependencyProperty.Register(
        "SelectedItem", 
        typeof (object), 
        typeof (BindableSelectedTreeViewItemBehavior), 
        new FrameworkPropertyMetadata(
          default(object), 
          FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));

    /// <summary>
    /// Gets or sets the selected item.
    /// </summary>
    public object SelectedItem
    {
      get { return GetValue(SelectedItemProperty); }

      set
      {
        if (value == SelectedItem) return;
        SetValue(SelectedItemProperty, value);
      }
    }

    #endregion

    #region fields

    #endregion

    #region methods

    /// <summary>
    /// Occurs when <see cref="SelectedItem"/> changes.
    /// </summary>
    /// <param name="sender">Not used.</param>
    /// <param name="e"></param>
    private static void OnSelectedItemChanged(
      DependencyObject sender, DependencyPropertyChangedEventArgs e)
    {
      var item = e.NewValue as TreeViewItem;
      if (item != null)
      {
        item.SetValue(TreeViewItem.IsSelectedProperty, true);
        return;
      }

      var behavior = (BindableSelectedTreeViewItemBehavior)sender;
      var treeView = behavior.AssociatedObject;
      if (treeView == null)
      {
        // at designtime the AssociatedObject sometimes seems to be null
        return;
      }

      item = GetTreeViewItem(treeView, e.NewValue);
      if (item != null)
      {
        item.IsSelected = true;
      }
    }

    /// <summary>
    /// Recursively search for an item in this subtree.
    /// </summary>
    /// <param name="container">
    /// The parent ItemsControl. This can be a TreeView or a TreeViewItem.
    /// </param>
    /// <param name="item">
    /// The item to search for.
    /// </param>
    /// <returns>
    /// The TreeViewItem that contains the specified item.
    /// </returns>
    private static TreeViewItem GetTreeViewItem(ItemsControl container, object item)
    {
      if (container == null) return null;

      if (container.DataContext == item)
      {
        return container as TreeViewItem;
      }

      // Expand the current container
      if (container is TreeViewItem && !((TreeViewItem)container).IsExpanded)
      {
        container.SetValue(TreeViewItem.IsExpandedProperty, true);
      }

      // Try to generate the ItemsPresenter and the ItemsPanel.
      // by calling ApplyTemplate.  Note that in the 
      // virtualizing case even if the item is marked 
      // expanded we still need to do this step in order to 
      // regenerate the visuals because they may have been virtualized away.
      container.ApplyTemplate();

      var itemsPresenter =
        (ItemsPresenter)container.Template.FindName("ItemsHost", container);

      if (itemsPresenter != null)
      {
        itemsPresenter.ApplyTemplate();
      }
      else
      {
        // The Tree template has not named the ItemsPresenter, 
        // so walk the descendents and find the child.
        itemsPresenter = container.GetVisualDescendent<ItemsPresenter>();

        if (itemsPresenter == null)
        {
          container.UpdateLayout();
          itemsPresenter = container.GetVisualDescendent<ItemsPresenter>();
        }
      }

      var itemsHostPanel = (Panel)VisualTreeHelper.GetChild(itemsPresenter, 0);

      // Ensure that the generator for this panel has been created.
      // ReSharper disable once UnusedVariable
      var children = itemsHostPanel.Children;
      
      var bringIndexIntoView = GetBringIndexIntoView(itemsHostPanel);
      for (int i = 0, count = container.Items.Count; i < count; i++)
      {
        TreeViewItem subContainer;
        if (bringIndexIntoView != null)
        {
          // Bring the item into view so 
          // that the container will be generated.
          bringIndexIntoView(i);
          subContainer =
            (TreeViewItem)container.ItemContainerGenerator.
                        ContainerFromIndex(i);
        }
        else
        {
          subContainer =
            (TreeViewItem)container.ItemContainerGenerator.
                        ContainerFromIndex(i);

          // Bring the item into view to maintain the 
          // same behavior as with a virtualizing panel.
          subContainer.BringIntoView();
        }

        if (subContainer == null)
        {
          continue;
        }

        // Search the next level for the object.
        var resultContainer = GetTreeViewItem(subContainer, item);
        if (resultContainer != null)
        {
          return resultContainer;
        }

        // The object is not under this TreeViewItem
        // so collapse it.
        subContainer.IsExpanded = false;
      }

      return null;
    }

    private static Action<int> GetBringIndexIntoView(Panel itemsHostPanel)
    {
      var virtualizingPanel = itemsHostPanel as VirtualizingStackPanel;
      if (virtualizingPanel == null)
      {
        return null;
      }

      var method = 
        virtualizingPanel.GetType().GetMethod(
          "BringIndexIntoView",
          BindingFlags.Instance | BindingFlags.NonPublic,
          Type.DefaultBinder,
          new[] { typeof(int) },
          null);

      if (method == null)
      {
        return null;
      }

      return i => method.Invoke(virtualizingPanel, new object[] { i });
    }

    /// <summary>
    /// Called after "AssociatedObject" is attached.
    /// </summary>
    protected override void OnAttached()
    {
      base.OnAttached();
      AssociatedObject.SelectedItemChanged += OnTreeViewSelectedItemChanged;
    }

    /// <summary>
    /// Called before "AssociatedObject" is detached.
    /// </summary>
    protected override void OnDetaching()
    {
      base.OnDetaching();
      if (AssociatedObject == null) return;

      AssociatedObject.SelectedItemChanged -= OnTreeViewSelectedItemChanged;
    }

    /// <summary>
    /// Occurs when the user changes the selected item of the tree located in the view by clicking.
    /// </summary>
    /// <param name="sender">Not used.</param>
    /// <param name="e">Not used.</param>
    private void OnTreeViewSelectedItemChanged(
      object sender, RoutedPropertyChangedEventArgs<object> e)
    {
      SelectedItem = e.NewValue;
    }

    #endregion
  }
}
